package de.pyranja.collections;

import java.lang.ref.SoftReference;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * <p>A thread safe single element cache that loads the cached element on demand. 
 * The cached element is internally held in a <code>SoftReference</code> and
 * may therefore be reclaimed by the garbage collector if it is currently not in
 * use. To ensure the desired behavior (strong) references to the cached element
 * as acquired through {@link #get() } should always be released as soon as 
 * possible.</p>  
 * 
 * <p>To allow on demand loading of the cached element, a {@link Callable} of
 * the appropriate type must be provided to new instances of this class. The
 * element will be loaded synchronously by the thread that demanded the empty
 * reference. Parallel requests for the cached element will block until the 
 * element is loaded.</p> 
 * 
 * <p>Typically this class should behave like a classic, lazily initialized
 * reference in applications with moderate memory demands, but may improve
 * memory footprint in applications that have high, spiky memory demands and 
 * where the cached element is only required infrequently.</p>
 * 
 * @author Chris Borckholder
 * @param <T> type of cached element
 */
public class ConcurrentCachedElement<T> {

	private final static String ERR_LOAD = "Failed to load cached element from ";
	
	// controls access to cached element
	private final ReentrantReadWriteLock lock;
	// cached element
	private SoftReference<T> cache;
	// loading method
	private final Callable<T> supplier;
	
	/**
	 * Creates a new cached element. The element is lazily loaded.
	 * @param supplier a {@link Callable} that is able to repeatedly produce instances 
	 * of the cached element 
	 */
	public ConcurrentCachedElement(Callable<T> supplier) {
		this.cache = new SoftReference<T>(null);
		this.supplier = supplier;
		this.lock = new ReentrantReadWriteLock();
	}
	
	/**
	 * Retrieves the cached elements. Either returns the available, cached 
	 * instance of the element or synchronously creates a new instance. While 
	 * creation of an instance takes place, this method will block until the 
	 * element is available again.
	 * 
	 * @return an instance of the cached element
	 * @throws IllegalStateException if refreshing the cached element failed
	 */
	public T get() {
		/*
		 * To synchronize parallel reads and ensure there is always at most one
		 * cached element, this method employs the double locked idiom based on
		 * a ReentrantReadWriteLock. Only threads that successfully aquired a
		 * write lock are allowed to invoke the recreation of the cached element.
		 */
		try {
			lock.readLock().lock();
			T element = cache.get();
			// check state of cached element
			if (element == null) {
				// element expired, we have to reload
				// upgrading a read lock is not possible
				lock.readLock().unlock();
				lock.writeLock().lock();
				// double check ; safe as we used the reentrant lock !
				element = cache.get();
				try {
					if (element == null) {
						// must have strong reference first to avoid gc 
						// prematurely collecting the soft reference
						element = refresh();						// reload element
						cache = new SoftReference<T>(element);	// cache reference
					}
				} finally {
					// downgrade lock in any case
					lock.readLock().lock();
					lock.writeLock().unlock();
				}
			}
			return element;
		} finally {
			// release lock in any case
			lock.readLock().unlock();
		}
	}
	
	// Tries to synchronously load the cached element
	// fails fast on error
	private T refresh() {
		try {
			return supplier.call();
		} catch (Exception e) {
			throw new IllegalStateException(ERR_LOAD + supplier,e);
		}
	}
	
	/**
	 * WARNING:
	 * This method intentionally breaks encapsulation to allow modifications by 
	 * a fake garbage collector.<strong>NEVER</strong> use this access in any
	 * productive setting. It will break the contract of this class and most
	 * likely the application.
	 */
	@Deprecated
	void setReference(final T element) {
		try {
			// simple locking
			lock.writeLock().lock();
			cache = new SoftReference<T>(element);
		} finally {
			lock.writeLock().unlock();
		}
	}
}
